<!DOCTYPE html>
<html>
  <head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
  <meta name="description" content="along&#39;s blog">
  <meta name="keyword" content="hexo-theme, vuejs">
  
    <link rel="shortcut icon" href="/css/images/logo.png">
  
  <title>
    
      搭建Vue SSR | ALONG
    
  </title>
  <link href="/cdn.along/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
  <link href="/cdn.along/nprogress/0.2.0/nprogress.min.css" rel="stylesheet">
  <link href="/cdn.along/highlight.js/9.12.0/styles/tomorrow.min.css" rel="stylesheet">
  
<link rel="stylesheet" href="/css/style.css">

  
  <script src="/cdn.along/jquery/3.2.1/jquery.min.js"></script>
  <script src="/cdn.along/geopattern/1.2.3/js/geopattern.min.js"></script>
  <script src="/cdn.along/nprogress/0.2.0/nprogress.min.js"></script>
  
    
<script src="/js/qrious.js"></script>

  
  
  
  
    <!-- MathJax support START -->
    <script type="text/x-mathjax-config">
      MathJax.Hub.Config({
        tex2jax: {
          inlineMath: [ ['$','$'], ["\\(","\\)"]  ],
          processEscapes: true,
          skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
        }
      });
    </script>

    <script type="text/x-mathjax-config">
      MathJax.Hub.Queue(function() {
        var all = MathJax.Hub.getAllJax(), i;
        for (i=0; i < all.length; i += 1) {
          all[i].SourceElement().parentNode.className += ' has-jax';
        }
      });
    </script>
    <script type="text/javascript" src="/cdn.along/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
    <!-- MathJax support END -->
  


  
  
    
<script src="/js/local-search.js"></script>


<meta name="generator" content="Hexo 5.4.0"></head>
<div class="wechat-share">
  <img src="/css/images/logo.png" />
</div>
  <body>
    <header class="header fixed-header">
  <div class="header-container">
    <a class="home-link" href="/">
      <div class="logo"></div>
      <span>ALONG</span>
    </a>
    <ul class="right-list">
      
        <li class="list-item">
          
            <a href="/" class="item-link">Home</a>
          
        </li>
      
        <li class="list-item">
          
            <a href="/tags/" class="item-link">Tags</a>
          
        </li>
      
        <li class="list-item">
          
            <a href="/archives/" class="item-link">Archives</a>
          
        </li>
      
        <li class="list-item">
          
            <a href="/about/" class="item-link">About</a>
          
        </li>
      
      
        <li class="menu-item menu-item-search right-list">
    <a role="button" class="popup-trigger">
        <i class="fa fa-search fa-fw"></i>
    </a>
</li>
      
    </ul>
    <div class="menu">
      <span class="icon-bar"></span>
      <span class="icon-bar"></span>
      <span class="icon-bar"></span>
    </div>
    <div class="menu-mask">
      <ul class="menu-list">
        
          <li class="menu-item">
            
              <a href="/" class="menu-link">Home</a>
            
          </li>
        
          <li class="menu-item">
            
              <a href="/tags/" class="menu-link">Tags</a>
            
          </li>
        
          <li class="menu-item">
            
              <a href="/archives/" class="menu-link">Archives</a>
            
          </li>
        
          <li class="menu-item">
            
              <a href="/about/" class="menu-link">About</a>
            
          </li>
        
      </ul>
    </div>
    
      <div class="search-pop-overlay">
    <div class="popup search-popup">
        <div class="search-header">
            <span class="search-icon">
                <i class="fa fa-search"></i>
            </span>
            <div class="search-input-container">
                <input autocomplete="off" autocapitalize="off"
                    placeholder="Please enter your keyword(s) to search." spellcheck="false"
                    type="search" class="search-input">
            </div>
            <span class="popup-btn-close">
                <i class="fa fa-times-circle"></i>
            </span>
        </div>
        <div id="search-result">
            <div id="no-result">
                <i class="fa fa-spinner fa-pulse fa-5x fa-fw"></i>
            </div>
        </div>
    </div>
</div>
    
  </div>
</header>

    <div id="article-banner">
  <h2>搭建Vue SSR</h2>
  <p class="post-date">2020-09-27</p>
  <div class="arrow-down">
    <a href="javascript:;"></a>
  </div>
</div>
<main class="app-body flex-box">
  <!-- Article START -->
  <article class="post-article">
    <section class="markdown-content"><h2 id="搭建VUE-SSR"><a href="#搭建VUE-SSR" class="headerlink" title="搭建VUE SSR"></a>搭建VUE SSR</h2><h3 id="环境依赖安装"><a href="#环境依赖安装" class="headerlink" title="环境依赖安装"></a>环境依赖安装</h3><p>初始化项目<code>npm init</code></p>
<p>安装依赖</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install vue vue-server-renderer</span><br></pre></td></tr></table></figure>

<h3 id="服务端生成html文本"><a href="#服务端生成html文本" class="headerlink" title="服务端生成html文本"></a>服务端生成html文本</h3><p>创建文件server.js</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">const Vue &#x3D; require(&#39;vue&#39;);</span><br><span class="line"></span><br><span class="line">const renderer &#x3D; require(&#39;vue-server-renderer&#39;).createRenderer();</span><br><span class="line"></span><br><span class="line">const app &#x3D; new Vue(&#123;</span><br><span class="line">    template: &#96;&lt;div id&#x3D;&quot;app&quot;&gt;</span><br><span class="line">        &lt;h1&gt;&#123;&#123; message &#125;&#125;&lt;&#x2F;h1&gt;</span><br><span class="line">    &lt;&#x2F;div&gt;&#96;,</span><br><span class="line">    data: &#123;</span><br><span class="line">        message: &#39;vue ssr&#39;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">renderer.renderToString(app, (err, html) &#x3D;&gt; &#123;</span><br><span class="line">    if (err) throw err</span><br><span class="line">    console.log(html);</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p>node执行文件<code>server.js</code></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;div id&#x3D;&quot;app&quot; data-server-rendered&#x3D;&quot;true&quot;&gt;&lt;h1&gt;vue ssr&lt;&#x2F;h1&gt;&lt;&#x2F;div&gt;</span><br></pre></td></tr></table></figure>

<h3 id="html文本发送到浏览器，搭建web服务"><a href="#html文本发送到浏览器，搭建web服务" class="headerlink" title="html文本发送到浏览器，搭建web服务"></a>html文本发送到浏览器，搭建web服务</h3><p>安装express</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install express</span><br></pre></td></tr></table></figure>
<p>express 并配置路由</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> express = <span class="built_in">require</span>(<span class="string">&#x27;express&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> server = express();</span><br><span class="line">server.get(<span class="string">&#x27;/&#x27;</span>, <span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">    renderer.renderToString(app, <span class="function">(<span class="params">err, html</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (err) res.status(<span class="number">500</span>).end(<span class="string">&#x27;Internal Server Error&#x27;</span>)</span><br><span class="line">        <span class="comment">// 设置响应头避免乱码</span></span><br><span class="line">        res.setHeader(<span class="string">&#x27;Content-Type&#x27;</span>, <span class="string">&#x27;text/html; charset=utf8&#x27;</span>)</span><br><span class="line">        res.end(html);</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">server.listen(<span class="number">3000</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">&#x27;server running at 3000&#x27;</span>)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p>启动项目，打开浏览器<code>localhost:3000</code></p>
<img src="/2020/09/27/%E6%90%AD%E5%BB%BAVue-SSR/1.png" class="">


<h3 id="使用模板渲染html"><a href="#使用模板渲染html" class="headerlink" title="使用模板渲染html"></a>使用模板渲染html</h3><p>创建<code>index-template.html</code></p>
<figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">&lt;!DOCTYPE <span class="meta-keyword">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">html</span> <span class="attr">lang</span>=<span class="string">&quot;en&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">&quot;UTF-8&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">name</span>=<span class="string">&quot;viewport&quot;</span> <span class="attr">content</span>=<span class="string">&quot;width=device-width, initial-scale=1.0&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">title</span>&gt;</span>&#123;&#123;title&#125;&#125;<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!--vue-ssr-outlet--&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>注意这里的<code>&lt;!--vue-ssr-outlet--&gt;</code>是模板替换的标识, 不能有额外的空格，如果格式错误或者没有匹配到这个标识，会报<code>Content placeholder not found in template.</code>的错误</p>
<p>server.js中使用模板,<code>createRenderer</code>添加配置</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">&#x27;fs&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> renderer = <span class="built_in">require</span>(<span class="string">&#x27;vue-server-renderer&#x27;</span>).createRenderer(&#123;</span><br><span class="line">    template: fs.readFileSync(<span class="string">&#x27;./index-template.html&#x27;</span>, <span class="string">&#x27;utf-8&#x27;</span>)</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>此后调用<code>renderer.renderToString</code>方法时会自动将参数vue实例渲染的内容添加到模板的指定位置中</p>
<p>模板中如果要传入数据可以通过给<code>renderer.renderToString(app, &#123;title: &#39;vue ssr&#39;&#125;, fn)</code>添加第二个参数，传入数据，可以直接在模板中使用<code>&#123;&#123;key&#125;&#125;</code>来渲染字符串，使用<code>&#123;&#123;&#123;key&#125;&#125;&#125;</code>三层花括号的方式渲染为html标签</p>
<img src="/2020/09/27/%E6%90%AD%E5%BB%BAVue-SSR/2.png" class="">

<h3 id="同构"><a href="#同构" class="headerlink" title="同构"></a>同构</h3><p>之前的代码服务端只能发送静态的html数据，对于双向绑定，点击事件等都无法生效，因为这些只能通过客户端处理才能生效。</p>
<p>创建目录<code>src</code></p>
<h4 id="创建源代码文件"><a href="#创建源代码文件" class="headerlink" title="创建源代码文件"></a>创建源代码文件</h4><p>src/App.vue</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line">    &lt;div id&#x3D;&#39;app&#39;&gt;</span><br><span class="line">        &lt;h1&gt;&#123;&#123;message&#125;&#125;&lt;&#x2F;h1&gt;</span><br><span class="line">        &lt;input v-model&#x3D;&quot;message&quot;&#x2F;&gt;</span><br><span class="line">        &lt;p&gt;</span><br><span class="line">            &lt;button @click&#x3D;&quot;click&quot;&gt;点击&lt;&#x2F;button&gt;</span><br><span class="line">        &lt;&#x2F;p&gt;</span><br><span class="line">    &lt;&#x2F;div&gt;</span><br><span class="line">&lt;&#x2F;template&gt;</span><br><span class="line">&lt;script&gt;</span><br><span class="line">export default &#123;</span><br><span class="line">    name: &#39;App&#39;,</span><br><span class="line">    components: &#123;&#125;,</span><br><span class="line">    data() &#123;</span><br><span class="line">        return &#123;</span><br><span class="line">            message: &#39;vue ssr&#39;</span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;,</span><br><span class="line">    methods: &#123;</span><br><span class="line">        click: () &#x3D;&gt; &#123;</span><br><span class="line">            console.log(&#39;点击&#39;)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    </span><br><span class="line">&#125;</span><br><span class="line">&lt;&#x2F;script&gt;</span><br></pre></td></tr></table></figure>

<p>src/app.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 通用应用入口</span></span><br><span class="line"><span class="keyword">import</span> Vue <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> App <span class="keyword">from</span> <span class="string">&#x27;./App.vue&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">createApp</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> app = <span class="keyword">new</span> Vue(&#123;</span><br><span class="line">        render: <span class="function"><span class="params">h</span> =&gt;</span> h(App)</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">return</span> &#123;app&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>src/entry-client.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 客户端入口</span></span><br><span class="line"><span class="keyword">import</span> &#123;createApp&#125; <span class="keyword">from</span> <span class="string">&#x27;./app&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> &#123;app&#125; = createApp();</span><br><span class="line"></span><br><span class="line">app.$mount(<span class="string">&#x27;#app&#x27;</span>);</span><br></pre></td></tr></table></figure>

<p>src/entry-server.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 服务端入口</span></span><br><span class="line"><span class="keyword">import</span> &#123;createApp&#125; <span class="keyword">from</span> <span class="string">&#x27;./app&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> content =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123;app&#125; = createApp();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 服务端路由，数据预取</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> app;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h4><p>通过源代码文件不能直接在服务端和客户端执行，比如.vue文件，需要借助webpack打包</p>
<p>生产依赖</p>
<ul>
<li><code>vue</code></li>
<li><code>vue-server-renderer</code>vue 服务端渲染工具</li>
<li><code>express</code></li>
<li><code>cross-env</code>npm区分环境变量</li>
</ul>
<p>开发依赖</p>
<ul>
<li><code>webpack</code> webpack核心包</li>
<li><code>webpack-cli</code>webpack命令行工具</li>
<li><code>webpack-merge</code> webpack配置合并</li>
<li><code>webpack-node-externals</code>排除webpack中的Node模块，在打包时排除掉例如fs等node模块</li>
<li><code>rimraf</code>基于node封装的跨平台rm -rf工具，可以借助命令行执行删除操作。这里用于打包前删除dist目录文件</li>
<li><code>friendly-errors-webpack-plugin</code>友好webpack错误提示</li>
<li><code>@babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader</code></li>
<li><code>vue-loader vue-template-compiler</code></li>
<li><code>file-loader</code> 处理字体资源</li>
<li><code>less less-loader</code></li>
<li><code>css-loader</code></li>
<li><code>url-loader</code> 处理图片资源</li>
</ul>
<h4 id="webpack配置"><a href="#webpack配置" class="headerlink" title="webpack配置"></a>webpack配置</h4><p>创建webpack配置文件</p>
<p>build/webpack.base.config.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> VueLoaderPlugin = <span class="built_in">require</span>(<span class="string">&#x27;vue-loader/lib/plugin&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">&#x27;path&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> FriendlyErrorsWebpackPlugin = <span class="built_in">require</span>(<span class="string">&#x27;friendly-errors-webpack-plugin&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> resolve = <span class="function"><span class="params">file</span> =&gt;</span> path.resolve(__dirname, file);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> isProd = process.env.NODE_ENV === <span class="string">&#x27;production&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">    mode: isProd ? <span class="string">&#x27;production&#x27;</span> : <span class="string">&#x27;development&#x27;</span>,</span><br><span class="line">    output: &#123;</span><br><span class="line">        path: resolve(<span class="string">&#x27;../dist/&#x27;</span>),</span><br><span class="line">        publicPath: <span class="string">&#x27;/dist/&#x27;</span>,</span><br><span class="line">        filename: <span class="string">&#x27;[name].[chunkhash].js&#x27;</span>,</span><br><span class="line">    &#125;,</span><br><span class="line">    resolve: &#123;</span><br><span class="line">        <span class="comment">// 路径别名</span></span><br><span class="line">        alias: &#123;</span><br><span class="line">            <span class="comment">// @ 指向 src</span></span><br><span class="line">            <span class="string">&#x27;@&#x27;</span>: resolve(<span class="string">&#x27;../src/&#x27;</span>)</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="comment">// 可以省略的扩展名</span></span><br><span class="line">        <span class="comment">// 当省略扩展名时。从前往后依次解析</span></span><br><span class="line">        extensions: [<span class="string">&#x27;.js&#x27;</span>, <span class="string">&#x27;.vue&#x27;</span>, <span class="string">&#x27;.json&#x27;</span>],</span><br><span class="line">    &#125;,</span><br><span class="line">    devtool: isProd ? <span class="string">&#x27;source-map&#x27;</span> : <span class="string">&#x27;cheap-module-eval-source-map&#x27;</span>,</span><br><span class="line">    <span class="built_in">module</span>: &#123;</span><br><span class="line">        roles: [</span><br><span class="line">            <span class="comment">// 处理图片资源</span></span><br><span class="line">            &#123;</span><br><span class="line">                test: <span class="regexp">/\.(png|jpg|gif)$/i</span>,</span><br><span class="line">                use: [</span><br><span class="line">                    &#123;</span><br><span class="line">                        loader: <span class="string">&#x27;url-loader&#x27;</span>,</span><br><span class="line">                        options: &#123;</span><br><span class="line">                            limit: <span class="number">8129</span>,</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line">                ]</span><br><span class="line">            &#125;,</span><br><span class="line">            <span class="comment">// 处理字体资源</span></span><br><span class="line">            &#123;</span><br><span class="line">                test: <span class="regexp">/\.(woff|woff2|eot|ttf|otf)$/</span>,</span><br><span class="line">                use: [</span><br><span class="line">                    <span class="string">&#x27;file-loader&#x27;</span></span><br><span class="line">                ]</span><br><span class="line">            &#125;,</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 处理.vue 资源</span></span><br><span class="line">            &#123;</span><br><span class="line">                test: <span class="regexp">/\.vue$/</span>,</span><br><span class="line">                loader: <span class="string">&#x27;vue-loader&#x27;</span>,</span><br><span class="line">            &#125;,</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 处理css</span></span><br><span class="line">            <span class="comment">// .css 文件，以及.vue中的&lt;style&gt;</span></span><br><span class="line">            &#123;</span><br><span class="line">                test: <span class="regexp">/\.css$/</span>,</span><br><span class="line">                use: [</span><br><span class="line">                    <span class="string">&#x27;vue-style-loader&#x27;</span>,</span><br><span class="line">                    <span class="string">&#x27;css-loader&#x27;</span>,</span><br><span class="line">                ]</span><br><span class="line">            &#125;,</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 处理less</span></span><br><span class="line">            &#123;</span><br><span class="line">                test: <span class="regexp">/\.less$/</span>,</span><br><span class="line">                use: [</span><br><span class="line">                    <span class="string">&#x27;vue-style-loader&#x27;</span>,</span><br><span class="line">                    <span class="string">&#x27;css-loader&#x27;</span>,</span><br><span class="line">                    <span class="string">&#x27;less-loader&#x27;</span>,</span><br><span class="line">                ]</span><br><span class="line">            &#125;</span><br><span class="line">        ]</span><br><span class="line">    &#125;,</span><br><span class="line">    plugins: [</span><br><span class="line">        <span class="keyword">new</span> VueLoaderPlugin(),</span><br><span class="line">        <span class="keyword">new</span> FriendlyErrorsWebpackPlugin(),</span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>build/webpack.client.config.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123;merge&#125; = <span class="built_in">require</span>(<span class="string">&#x27;webpack-merge&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> baseConfig = <span class="built_in">require</span>(<span class="string">&#x27;./webpack.base.config.js&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> VueSSRClientPlugin = <span class="built_in">require</span>(<span class="string">&#x27;vue-server-renderer/client-plugin&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = merge(baseConfig, &#123;</span><br><span class="line">    entry: &#123;</span><br><span class="line">        <span class="comment">// 相对路径取决于执行打包命令的路径</span></span><br><span class="line">        app: <span class="string">&#x27;./src/entry-client.js&#x27;</span> </span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="built_in">module</span>: &#123;</span><br><span class="line">        rules: [</span><br><span class="line">            <span class="comment">// ES6 转 ES5，客户端单独处理，node端支持es6</span></span><br><span class="line">            &#123;</span><br><span class="line">                test: <span class="regexp">/\.m?js$/</span>,</span><br><span class="line">                exclude: <span class="regexp">/(node_modules|bower_components)/</span>,</span><br><span class="line">                use: &#123;</span><br><span class="line">                    loader: <span class="string">&#x27;babel-loader&#x27;</span>,</span><br><span class="line">                    options: &#123;</span><br><span class="line">                        presets: [<span class="string">&#x27;@babel/preset-env&#x27;</span>],</span><br><span class="line">                        cacheDirectory: <span class="literal">true</span>,</span><br><span class="line">                        plugins: [<span class="string">&#x27;@babel/plugin-transform-runtime&#x27;</span>]</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        ]</span><br><span class="line">    &#125;,</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 将webpack运行时分离到一个引导chunk中</span></span><br><span class="line">    <span class="comment">// 以便可以在之后正确注入异步chunk</span></span><br><span class="line">    optimization: &#123;</span><br><span class="line">        splitChunks: &#123;</span><br><span class="line">            name: <span class="string">&quot;manifest&quot;</span>,</span><br><span class="line">            minChunks: <span class="literal">Infinity</span>,</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;,</span><br><span class="line"></span><br><span class="line">    plugins: [</span><br><span class="line">        <span class="comment">// 在输出目录中生成 vue-ssr-client-manifest.json</span></span><br><span class="line">        <span class="keyword">new</span> VueSSRClientPlugin()</span><br><span class="line">    ]</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p>build/webpack.server.config.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123;merge&#125; = <span class="built_in">require</span>(<span class="string">&#x27;webpack-merge&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> nodeExternals = <span class="built_in">require</span>(<span class="string">&#x27;webpack-node-externals&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> baseConfig = <span class="built_in">require</span>(<span class="string">&#x27;./webpack.base.config&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> VueSSRServerPlugin = <span class="built_in">require</span>(<span class="string">&#x27;vue-server-renderer/server-plugin&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = merge(baseConfig, &#123;</span><br><span class="line">    entry: <span class="string">&#x27;./src/entry-server.js&#x27;</span>,</span><br><span class="line">    <span class="comment">// 允许webpack 以Node适用方式处理模块加载</span></span><br><span class="line">    <span class="comment">// 并且还会在编译Vue组件时，</span></span><br><span class="line">    <span class="comment">// 告知vue-loader ,输出面向服务器的代码（server-oriented code）</span></span><br><span class="line">    target: <span class="string">&#x27;node&#x27;</span>,</span><br><span class="line">    output: &#123;</span><br><span class="line">        filename: <span class="string">&#x27;server-bundle.js&#x27;</span>,</span><br><span class="line">        <span class="comment">// 配置server bundle 用Node风格(module.exports)导出模块（Node-style exports）</span></span><br><span class="line">        libraryTarget: <span class="string">&#x27;commonjs2&#x27;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// 不打包node_modules第三方包，保留require方式加载</span></span><br><span class="line">    <span class="comment">// 因为nodejs中一些node第三方包本身就是适用commonjs规范导入导出，在node环境不需要打包</span></span><br><span class="line">    externals: [nodeExternals(&#123;</span><br><span class="line">        <span class="comment">// 白名单中资源蒸尝大包，第三方包中的css资源依然需要打包</span></span><br><span class="line">        allowlist: [<span class="regexp">/\.css$/</span>]</span><br><span class="line">    &#125;)],</span><br><span class="line"></span><br><span class="line">    plugins: [</span><br><span class="line">        <span class="comment">// 生成默认名称为vue-ssr-server-bundle.json的文件</span></span><br><span class="line">        <span class="keyword">new</span> VueSSRServerPlugin()</span><br><span class="line">    ]</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<h4 id="配置打包命令"><a href="#配置打包命令" class="headerlink" title="配置打包命令"></a>配置打包命令</h4><p>package.json</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="attr">&quot;scripts&quot;</span>: &#123;</span><br><span class="line">        <span class="attr">&quot;build:client&quot;</span>: <span class="string">&quot;cross-env NODE_ENV=production webpack --config build/webpack.client.config.js&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;build:server&quot;</span>: <span class="string">&quot;cross-env NODE_ENV=production webpack --config build/webpack.server.config.js&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;build&quot;</span>: <span class="string">&quot;rimraf dist &amp;&amp; npm run build:client &amp;&amp; npm run build:server&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;start&quot;</span>: <span class="string">&quot;cross-env NODE_ENV=production node server.js&quot;</span>,</span><br><span class="line">        <span class="attr">&quot;dev&quot;</span>: <span class="string">&quot;node server.js&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h5 id="脚本测试"><a href="#脚本测试" class="headerlink" title="脚本测试"></a>脚本测试</h5><ul>
<li>客户端打包<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm run build:client</span><br></pre></td></tr></table></figure>

</li>
</ul>
<p>项目中多出了<code>dist</code>目录</p>
<ul>
<li>服务端打包</li>
</ul>
<p>先删除dist目录</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm run build:server</span><br></pre></td></tr></table></figure>

<p>服务端打包只生成了一个文件<code>vue-ssr-server-bundle.json</code></p>
<ul>
<li>双端打包</li>
</ul>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm run build</span><br></pre></td></tr></table></figure>

<h4 id="启动同构服务"><a href="#启动同构服务" class="headerlink" title="启动同构服务"></a>启动同构服务</h4><p>修改server.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 服务端打包生成的文件</span></span><br><span class="line"><span class="keyword">const</span> serverBundle = <span class="built_in">require</span>(<span class="string">&#x27;./dist/vue-ssr-server-bundle.json&#x27;</span>);</span><br><span class="line"><span class="comment">// 客户端打包生成的文件</span></span><br><span class="line"><span class="keyword">const</span> clientManifest = <span class="built_in">require</span>(<span class="string">&#x27;./dist/vue-ssr-client-manifest.json&#x27;</span>);</span><br><span class="line"><span class="comment">// render时vue实例实例的嵌套模板</span></span><br><span class="line"><span class="keyword">const</span> template = fs.readFileSync(<span class="string">&#x27;./index-template.html&#x27;</span>, <span class="string">&#x27;utf-8&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> renderer = <span class="built_in">require</span>(<span class="string">&#x27;vue-server-renderer&#x27;</span>).createBundleRenderer(serverBundle ,&#123;</span><br><span class="line">    template,</span><br><span class="line">    clientManifest</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>

<p>客户端请求js资源，处理dist下文件可访问</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 客户端请求js资源，处理dist下文件可访问</span></span><br><span class="line">server.use(<span class="string">&#x27;/dist&#x27;</span>, express.static(<span class="string">&#x27;./dist&#x27;</span>));</span><br></pre></td></tr></table></figure>

<p>删除server.js中创建的vue实例，并修改路由<code>/</code>的内容</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">server.get(<span class="string">&#x27;/&#x27;</span>, <span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// renderer.renderToString(app, &#123;title: &#x27;vue ssr&#x27;&#125;, (err, html) =&gt; &#123;</span></span><br><span class="line">    renderer.renderToString(&#123;<span class="attr">title</span>: <span class="string">&#x27;vue ssr&#x27;</span>&#125;, <span class="function">(<span class="params">err, html</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (err) res.status(<span class="number">500</span>).end(<span class="string">&#x27;Internal Server Error&#x27;</span>)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置响应头避免乱码</span></span><br><span class="line">        res.setHeader(<span class="string">&#x27;Content-Type&#x27;</span>, <span class="string">&#x27;text/html; charset=utf8&#x27;</span>)</span><br><span class="line">        res.end(html);</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p>renderer.renderToString 不用再传入vue实例，他会自动从<code>vue-ssr-server-bundle.json</code>找到渲染内容</p>
<p>启动服务</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm run start</span><br></pre></td></tr></table></figure>

<p>浏览器访问<code>localhost:3000</code>,可以看到输出了页面，并且v-model和click时间也生效了</p>
<img src="/2020/09/27/%E6%90%AD%E5%BB%BAVue-SSR/3.png" class="">

<p>在同构应用中，服务端生成了静态html，客户端不会再次重新渲染，而是激活页面的交互效果，这是一种混合模式</p>
<blockquote>
<p>在开发模式下，Vue 将推断客户端生成的虚拟 DOM 树 (virtual DOM tree)，是否与从服务器渲染的 DOM 结构 (DOM structure) 匹配。如果无法匹配，它将退出混合模式，丢弃现有的 DOM 并从头开始渲染。在生产模式下，此检测会被跳过，以避免性能损耗</p>
</blockquote>
<p><a target="_blank" rel="noopener" href="https://ssr.vuejs.org/zh/guide/hydration.html">参考Vue SSR客户端激活</a></p>
<h3 id="配置生产环境"><a href="#配置生产环境" class="headerlink" title="配置生产环境"></a>配置生产环境</h3><p>安装chokidar,这个包封装了node中的fs.watch fs.watchFile,处理了其中的一些问题，用于监视文件的变化</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install chokidar</span><br></pre></td></tr></table></figure>

<p>安装webpack-dev-middleware, 用于在开发环境将打包结果保存在内存中，避免频繁的文件读写操作</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install webpack-dev-middleware</span><br></pre></td></tr></table></figure>

<p>安装<a href="https://github.com/webpack-contrib/webpack-hot-middleware">webpack-hot-middleware</a>帮助我们自动在打包后刷新页面，热更新</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --save-dev webpack-hot-middleware</span><br></pre></td></tr></table></figure>

<p>修改 server.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> Vue = <span class="built_in">require</span>(<span class="string">&#x27;vue&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> express = <span class="built_in">require</span>(<span class="string">&#x27;express&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">&#x27;fs&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> &#123;createBundleRenderer&#125; = <span class="built_in">require</span>(<span class="string">&#x27;vue-server-renderer&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> setupDevServer = <span class="built_in">require</span>(<span class="string">&#x27;./build/setup-dev-server&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> isProd = process.env.NODE_ENV === <span class="string">&#x27;production&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// const renderer = require(&#x27;vue-server-renderer&#x27;).createRenderer(&#123;</span></span><br><span class="line"><span class="comment">//     template: fs.readFileSync(&#x27;./index-template.html&#x27;, &#x27;utf-8&#x27;)</span></span><br><span class="line"><span class="comment">// &#125;);</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> server = express();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 客户端请求js资源，处理dist下文件可访问</span></span><br><span class="line">server.use(<span class="string">&#x27;/dist&#x27;</span>, express.static(<span class="string">&#x27;./dist&#x27;</span>));</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> renderer;</span><br><span class="line"><span class="keyword">let</span> onReady; <span class="comment">// 用于保存开发模式renderer渲染器赋值状态</span></span><br><span class="line"><span class="keyword">if</span> (isProd) &#123;</span><br><span class="line">    <span class="comment">// 服务端打包生成的文件</span></span><br><span class="line">    <span class="keyword">const</span> serverBundle = <span class="built_in">require</span>(<span class="string">&#x27;./dist/vue-ssr-server-bundle.json&#x27;</span>);</span><br><span class="line">    <span class="comment">// 客户端打包生成的文件</span></span><br><span class="line">    <span class="keyword">const</span> clientManifest = <span class="built_in">require</span>(<span class="string">&#x27;./dist/vue-ssr-client-manifest.json&#x27;</span>);</span><br><span class="line">    <span class="comment">// render时vue实例实例的嵌套模板</span></span><br><span class="line">    <span class="keyword">const</span> template = fs.readFileSync(<span class="string">&#x27;./index-template.html&#x27;</span>, <span class="string">&#x27;utf-8&#x27;</span>);</span><br><span class="line">    renderer = createBundleRenderer(serverBundle, &#123;</span><br><span class="line">        template,</span><br><span class="line">        clientManifest</span><br><span class="line">    &#125;);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 开发模式 =》 监视打包构建 =》 重新生成renderer渲染器</span></span><br><span class="line">    onReady = setupDevServer(server, <span class="function">(<span class="params">serverBundle, template, clientManifest</span>) =&gt;</span> &#123;</span><br><span class="line">        renderer = createBundleRenderer(serverBundle, &#123;</span><br><span class="line">            template,</span><br><span class="line">            clientManifest</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// const app = new Vue(&#123;</span></span><br><span class="line"><span class="comment">//     template: `&lt;div id=&quot;app&quot;&gt;</span></span><br><span class="line"><span class="comment">//         &lt;h1&gt;&#123;&#123; message &#125;&#125;&lt;/h1&gt;</span></span><br><span class="line"><span class="comment">//     &lt;/div&gt;`,</span></span><br><span class="line"><span class="comment">//     data: &#123;</span></span><br><span class="line"><span class="comment">//         message: &#x27;vue ssr&#x27;</span></span><br><span class="line"><span class="comment">//     &#125;</span></span><br><span class="line"><span class="comment">// &#125;)</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> render = <span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// renderer.renderToString(app, &#123;title: &#x27;vue ssr&#x27;&#125;, (err, html) =&gt; &#123;</span></span><br><span class="line">    renderer.renderToString(&#123; <span class="attr">title</span>: <span class="string">&#x27;vue ssr&#x27;</span> &#125;, <span class="function">(<span class="params">err, html</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (err) res.status(<span class="number">500</span>).end(<span class="string">&#x27;Internal Server Error&#x27;</span>)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置响应头避免乱码</span></span><br><span class="line">        res.setHeader(<span class="string">&#x27;Content-Type&#x27;</span>, <span class="string">&#x27;text/html; charset=utf8&#x27;</span>)</span><br><span class="line">        res.end(html);</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">server.get(<span class="string">&#x27;/&#x27;</span>, </span><br><span class="line">    isProd ? render : <span class="keyword">async</span> (req, res) =&gt; &#123;</span><br><span class="line">        <span class="comment">// 开发模式要在有了打包结果并且渲染器赋值完成才执行render</span></span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;等待 打包&#x27;</span>)</span><br><span class="line">        <span class="keyword">await</span> onReady;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;打包 done&#x27;</span>)</span><br><span class="line">        render(req, res);</span><br><span class="line">    &#125;</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">server.listen(<span class="number">3000</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">&#x27;server running at 3000&#x27;</span>)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p>setup-dev-server.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> fs = <span class="built_in">require</span>(<span class="string">&#x27;fs&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">&#x27;path&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> chokidar = <span class="built_in">require</span>(<span class="string">&#x27;chokidar&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> webpack = <span class="built_in">require</span>(<span class="string">&#x27;webpack&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> devMiddleware = <span class="built_in">require</span>(<span class="string">&#x27;webpack-dev-middleware&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> hotMiddleware = <span class="built_in">require</span>(<span class="string">&#x27;webpack-hot-middleware&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> resolve = <span class="function"><span class="params">file</span> =&gt;</span> path.resolve(__dirname, file);</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function">(<span class="params">server, callback</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">let</span> ready; <span class="comment">// 用于保存promise中的resolve函数</span></span><br><span class="line">    <span class="keyword">const</span> onReady = <span class="keyword">new</span> <span class="built_in">Promise</span>(<span class="function"><span class="params">r</span> =&gt;</span> ready = r);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 监视构建过程 -&gt; 更新 Renderer</span></span><br><span class="line">    <span class="keyword">let</span> template;</span><br><span class="line">    <span class="keyword">let</span> serverBundle;</span><br><span class="line">    <span class="keyword">let</span> clientManifest;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> update = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (template &amp;&amp; serverBundle &amp;&amp; clientManifest) &#123;</span><br><span class="line">            ready();</span><br><span class="line">            callback(serverBundle, template, clientManifest)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 监视构建 template -&gt; 调用update -&gt; 更新renderer渲染器</span></span><br><span class="line">    <span class="keyword">const</span> tempaltePath = path.resolve(__dirname, <span class="string">&#x27;../index-template.html&#x27;</span>);</span><br><span class="line">    template = fs.readFileSync(tempaltePath, <span class="string">&#x27;utf-8&#x27;</span>);</span><br><span class="line">    chokidar.watch(tempaltePath).on(<span class="string">&#x27;change&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">        <span class="comment">// 当文件变化时</span></span><br><span class="line">        template = fs.readFileSync(tempaltePath, <span class="string">&#x27;utf-8&#x27;</span>);</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;模板加载读取完成&#x27;</span>)</span><br><span class="line">        update();</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 监视构建 serverBundle -&gt; 调用update -&gt; 更新renderer渲染器</span></span><br><span class="line">    <span class="keyword">const</span> serverConfig = <span class="built_in">require</span>(<span class="string">&#x27;./webpack.server.config&#x27;</span>);</span><br><span class="line">    <span class="keyword">const</span> serverCompiler = webpack(serverConfig);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// webapck的编译器自带文件监视api,但是在开发环境使用会导致频繁的文件打包，文件读写，所以需要借助webpack-dev-middleware实现文件打包内容缓存在内存中</span></span><br><span class="line">    <span class="comment">// serverCompiler.watch(&#123;&#125;, (err, status) =&gt; &#123;</span></span><br><span class="line">    <span class="comment">//     if (err) throw err;</span></span><br><span class="line">    <span class="comment">//     if (status.hasErrors()) return;</span></span><br><span class="line">    <span class="comment">//     serverBundle = JSON.parse(</span></span><br><span class="line">    <span class="comment">//         fs.readFileSync(resolve(&#x27;../dist/vue-ssr-server-bundle.json&#x27;), &#x27;utf-8&#x27;)</span></span><br><span class="line">    <span class="comment">//     );</span></span><br><span class="line">    <span class="comment">//     console.log(serverBundle);</span></span><br><span class="line">    <span class="comment">//     update();</span></span><br><span class="line">    <span class="comment">// &#125;)</span></span><br><span class="line">    <span class="keyword">const</span> serverDevMiddleware = devMiddleware(serverCompiler, &#123;</span><br><span class="line">        logLevel: <span class="string">&#x27;silent&#x27;</span> <span class="comment">// 关闭日志输出，有friendlyErrorsWebpackPlugin 处理</span></span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="comment">// serverCompiler.hooks.done.tap添加打包后的回调函数，从而调用update函数，第一个参数&#x27;server&#x27;是我们定义的事件名字，没有固定意义</span></span><br><span class="line">    serverCompiler.hooks.done.tap(<span class="string">&#x27;server&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">        serverBundle = <span class="built_in">JSON</span>.parse(</span><br><span class="line">            <span class="comment">// serverDevMiddleware.fileSystem 与 fs 类似，只不过是操作内存中的文件</span></span><br><span class="line">            serverDevMiddleware.fileSystem.readFileSync(resolve(<span class="string">&#x27;../dist/vue-ssr-server-bundle.json&#x27;</span>), <span class="string">&#x27;utf-8&#x27;</span>)</span><br><span class="line">        );</span><br><span class="line">        <span class="comment">// console.log(serverBundle);</span></span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;服务端打包完成&#x27;</span>)</span><br><span class="line">        update();</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 监视构建 clientManifest -&gt; 调用update -&gt; 更新renderer渲染器</span></span><br><span class="line">    <span class="keyword">const</span> clientConfig = <span class="built_in">require</span>(<span class="string">&#x27;./webpack.client.config&#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 配置热更新</span></span><br><span class="line">    clientConfig.plugins.push(<span class="keyword">new</span> webpack.HotModuleReplacementPlugin());</span><br><span class="line">    clientConfig.entry.app = [</span><br><span class="line">        <span class="string">&#x27;webpack-hot-middleware/client?quiet=true&amp;reload=true&#x27;</span>, <span class="comment">// 和服务端交互处理热更新的客户端脚本， 不会刷新页面</span></span><br><span class="line">        <span class="comment">// ? 之后可附带参数，quiet 指热更新时控制台禁止输出热更新日志，（[HMR] bundle rebuilding[HMR] bundle rebuilt in 33ms这种）</span></span><br><span class="line">        <span class="comment">// reload 代表在热更新卡住时刷新界面</span></span><br><span class="line">        <span class="comment">// 更多使用参照 https://github.com/webpack-contrib/webpack-hot-middleware</span></span><br><span class="line">        clientConfig.entry.app, <span class="comment">// webpack中配置的原本的打包入口</span></span><br><span class="line">    ];</span><br><span class="line">    clientConfig.output.filename = <span class="string">&#x27;[name].js&#x27;</span>; <span class="comment">// 热更新模式下，确保文件名一致，如果在配置文件中配置了[chunkhash]等导致输出的文件名不一致，热更新编译时会报错Cannot use [chunkhash] or [contenthash] for chunk in &#x27;[name].[chunkhash].js&#x27; (use [hash] instead)</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> clientCompiler = webpack(clientConfig);</span><br><span class="line">    <span class="keyword">const</span> clientDevMiddleware = devMiddleware(clientCompiler, &#123;</span><br><span class="line">        publicPath: clientConfig.output.publicPath,</span><br><span class="line">        logLevel: <span class="string">&#x27;silent&#x27;</span> <span class="comment">// 关闭日志输出，有friendlyErrorsWebpackPlugin 处理</span></span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="comment">// clientCompiler.hooks.done.tap添加打包后的回调函数，从而调用update函数，第一个参数&#x27;client&#x27;是我们定义的事件名字，没有固定意义</span></span><br><span class="line">    clientCompiler.hooks.done.tap(<span class="string">&#x27;client&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">        clientManifest = <span class="built_in">JSON</span>.parse(</span><br><span class="line">            <span class="comment">// clientDevMiddleware.fileSystem 与 fs 类似，只不过是操作内存中的文件</span></span><br><span class="line">            clientDevMiddleware.fileSystem.readFileSync(resolve(<span class="string">&#x27;../dist/vue-ssr-client-manifest.json&#x27;</span>), <span class="string">&#x27;utf-8&#x27;</span>)</span><br><span class="line">        );</span><br><span class="line">        <span class="comment">// console.log(clientManifest);</span></span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;客户端打包完成&#x27;</span>)</span><br><span class="line">        update();</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 配置热更新</span></span><br><span class="line">    server.use(hotMiddleware(clientCompiler, &#123;</span><br><span class="line">        log: <span class="literal">false</span>, <span class="comment">// 关闭本身的日志输出</span></span><br><span class="line">    &#125;));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 在server.js我们使用user static 处理dist下的文件为静态资源,但是开发环境中打包文件在内存中，没有实际的静态资源，所以回到值客户端不能激活</span></span><br><span class="line">    <span class="comment">// clientDevMiddleware 中间件提供了对内存中数据的访问，当客户端访问 /dist/下某个js文件时，会尝试返回内存中的文件数据</span></span><br><span class="line">    server.use(clientDevMiddleware)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> onReady;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>到此为止，执行<code>npm run dev</code>,会帮我们启动一个开发服务，包含了客户端服务端同构、热更新的功能</p>
<h3 id="配置路由"><a href="#配置路由" class="headerlink" title="配置路由"></a>配置路由</h3><p>安装<code>vue-router</code></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm i vue-router --save</span><br></pre></td></tr></table></figure>

<p>创建三个路由相关的页面文件</p>
<p>src/pages/404.vue、src/pages/About.vue、src/pages/Home.vue</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/pages/404.vue</span></span><br><span class="line">&lt;template&gt;</span><br><span class="line">    &lt;div <span class="class"><span class="keyword">class</span></span>=<span class="string">&#x27;page404&#x27;</span>&gt;<span class="number">404</span> Not Page&lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line">&lt;script&gt;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">    name: <span class="string">&#x27;page404&#x27;</span>,</span><br><span class="line">&#125;</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"><span class="comment">// src/pages/About.vue</span></span><br><span class="line">&lt;template&gt;</span><br><span class="line">    &lt;div <span class="class"><span class="keyword">class</span></span>=<span class="string">&#x27;about&#x27;</span>&gt;关于&lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line">&lt;script&gt;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">    name: <span class="string">&#x27;about&#x27;</span>,</span><br><span class="line">&#125;</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"><span class="comment">// src/pages/Home.vue</span></span><br><span class="line">&lt;template&gt;</span><br><span class="line">    &lt;div <span class="class"><span class="keyword">class</span></span>=<span class="string">&#x27;home&#x27;</span>&gt;首页&lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line">&lt;script&gt;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">    name: <span class="string">&#x27;home&#x27;</span>,</span><br><span class="line">&#125;</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure>


<p>server.js 稍作修改</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">// render函数renderToString 中第一个参数填加请求路由req.url</span></span><br><span class="line"><span class="keyword">const</span> render = <span class="keyword">async</span> (req, res) =&gt; &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// renderer.renderToString(app, &#123;title: &#x27;vue ssr&#x27;&#125;, (err, html) =&gt; &#123;</span></span><br><span class="line">        <span class="comment">// renderToString 中第一个参数的url会在entry-server中通过context.url使用</span></span><br><span class="line">        <span class="keyword">const</span> html = <span class="keyword">await</span> renderer.renderToString(&#123; <span class="attr">title</span>: <span class="string">&#x27;vue ssr&#x27;</span>, <span class="attr">url</span>: req.url &#125;)</span><br><span class="line">        <span class="comment">// 设置响应头避免乱码</span></span><br><span class="line">        res.setHeader(<span class="string">&#x27;Content-Type&#x27;</span>, <span class="string">&#x27;text/html; charset=utf8&#x27;</span>)</span><br><span class="line">        res.end(html);</span><br><span class="line">    &#125; <span class="keyword">catch</span>(err) &#123;</span><br><span class="line">        res.status(<span class="number">500</span>).end(<span class="string">&#x27;Internal Server Error&#x27;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 服务端渲染不用对每一个路径都匹配路由，只需要通过*匹配所有路由，vuerouter中会对未匹配的路由返回404页面</span></span><br><span class="line">server.get(<span class="string">&#x27;*&#x27;</span>, </span><br><span class="line">    isProd ? render : <span class="keyword">async</span> (req, res) =&gt; &#123;</span><br><span class="line">        <span class="comment">// 开发模式要在有了打包结果并且渲染器赋值完成才执行render</span></span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;等待 打包&#x27;</span>)</span><br><span class="line">        <span class="keyword">await</span> onReady;</span><br><span class="line">        <span class="built_in">console</span>.log(<span class="string">&#x27;打包 done&#x27;</span>)</span><br><span class="line">        render(req, res);</span><br><span class="line">    &#125;</span><br><span class="line">)</span><br></pre></td></tr></table></figure>

<p>创建router/index.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> Vue <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> VueRouter <span class="keyword">from</span> <span class="string">&#x27;vue-router&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> Home <span class="keyword">from</span> <span class="string">&#x27;@/pages/Home&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> Page404 <span class="keyword">from</span> <span class="string">&#x27;@/pages/404&#x27;</span>; </span><br><span class="line"></span><br><span class="line">Vue.use(VueRouter);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 类似于 createApp，我们也需要给每个请求一个新的 router 实例，所以文件导出一个 createRouter 函数</span></span><br><span class="line"><span class="comment">// 防止数据污染</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> createRouter = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> router = <span class="keyword">new</span> VueRouter(&#123;</span><br><span class="line">        mode: <span class="string">&#x27;history&#x27;</span>, <span class="comment">// 服务端不支持hash模式</span></span><br><span class="line">        routes: [</span><br><span class="line">            &#123;</span><br><span class="line">                path: <span class="string">&#x27;/&#x27;</span>,</span><br><span class="line">                name: <span class="string">&#x27;home&#x27;</span>,</span><br><span class="line">                component: Home</span><br><span class="line">            &#125;,</span><br><span class="line">            &#123;</span><br><span class="line">                path: <span class="string">&#x27;/about&#x27;</span>,</span><br><span class="line">                name: <span class="string">&#x27;about&#x27;</span>,</span><br><span class="line">                component: <span class="function">() =&gt;</span> <span class="keyword">import</span>(<span class="string">&#x27;@/page/About&#x27;</span>)</span><br><span class="line">            &#125;,</span><br><span class="line">            &#123;</span><br><span class="line">                path: <span class="string">&#x27;*&#x27;</span>, <span class="comment">// 上面的路由未匹配到时，可以通过*匹配到404页面</span></span><br><span class="line">                name: <span class="string">&#x27;page404&#x27;</span>,</span><br><span class="line">                component: Page404</span><br><span class="line">            &#125;</span><br><span class="line">        ]</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">return</span> router;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>重写src/entry-server.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123;createApp&#125; <span class="keyword">from</span> <span class="string">&#x27;./app&#x27;</span>;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> context =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123;app, router&#125; = createApp();</span><br><span class="line"></span><br><span class="line">    router.push(context.url);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">await</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(router.onReady.bind(router));</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> app;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>重写src/entry-client.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123;createApp&#125; <span class="keyword">from</span> <span class="string">&#x27;./app&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> &#123;app, router&#125; = createApp();</span><br><span class="line"></span><br><span class="line">router.onReady(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    app.$mount(<span class="string">&#x27;#app&#x27;</span>);</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p>修改src/app.js,添加vue实例的路由</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> Vue <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> App <span class="keyword">from</span> <span class="string">&#x27;./App.vue&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123;createRouter&#125; <span class="keyword">from</span> <span class="string">&#x27;./router&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">createApp</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> router = createRouter();</span><br><span class="line">    <span class="keyword">const</span> app = <span class="keyword">new</span> Vue(&#123;</span><br><span class="line">        router,</span><br><span class="line">        render: <span class="function"><span class="params">h</span> =&gt;</span> h(App)</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">return</span> &#123;app, router&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>修改src/App.vue,添加路由导航和出口</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&lt;div id=<span class="string">&#x27;app&#x27;</span>&gt;</span><br><span class="line">    &lt;ul&gt;</span><br><span class="line">        &lt;li&gt;<span class="xml"><span class="tag">&lt;<span class="name">router-link</span> <span class="attr">to</span>=<span class="string">&quot;/&quot;</span>&gt;</span>home<span class="tag">&lt;/<span class="name">router-link</span>&gt;</span></span>&lt;/li&gt;</span><br><span class="line">        &lt;li&gt;<span class="xml"><span class="tag">&lt;<span class="name">router-link</span> <span class="attr">to</span>=<span class="string">&quot;/about&quot;</span>&gt;</span>about<span class="tag">&lt;/<span class="name">router-link</span>&gt;</span></span>&lt;/li&gt;</span><br><span class="line">    &lt;/ul&gt;</span><br><span class="line">    &lt;!-- 路由出口 --&gt;</span><br><span class="line">    &lt;router-view/&gt;</span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure>

<p>至此，<code>npm run dev</code>启动服务已经可以看到页面上的路由生效了，并且，首次加载之后，页面的跳转是通过客户端懒加载来实现的，这样我们的应用既可以实现更好的seo,又能给用户提供单页面应用极致的用户体验</p>
<img src="/2020/09/27/%E6%90%AD%E5%BB%BAVue-SSR/router.png" class="">

<p>更多参照<a target="_blank" rel="noopener" href="https://ssr.vuejs.org/zh/guide/routing.html#%E4%BD%BF%E7%94%A8-vue-router-%E7%9A%84%E8%B7%AF%E7%94%B1">Vue SSR 路由和代码分割</a></p>
<h3 id="配置不同界面的Head标签"><a href="#配置不同界面的Head标签" class="headerlink" title="配置不同界面的Head标签"></a>配置不同界面的Head标签</h3><p>之前我们通过index-template模板给所有页面配置了统一的模板，如果我们需要针对摸个路由配置不同的页面title，或者meta标签等，可以借助vue-meta</p>
<p>安装vue-meta</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install vue-meta</span><br></pre></td></tr></table></figure>

<p>在同一入口src/app.js中混入vue-meta</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> VueMeta <span class="keyword">from</span> <span class="string">&#x27;vue-meta&#x27;</span>;</span><br><span class="line">Vue.use(VueMeta);</span><br><span class="line">Vue.mixin(&#123;</span><br><span class="line">    metaInfo: &#123;</span><br><span class="line">        titleTemplate: <span class="string">&#x27;%s - Vue SSR&#x27;</span>, <span class="comment">// title 的模板，%s 会替换为标题</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;)</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>修改src/entry-server.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> context =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123;app, router&#125; = createApp();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 拿到混入的meta内容</span></span><br><span class="line">    <span class="keyword">const</span> meta = app.$meta();</span><br><span class="line"></span><br><span class="line">    router.push(context.url);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 注入到context上下文中，这样在页面模板中就可以取到meta的内容了</span></span><br><span class="line">    context.meta = meta;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">await</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(router.onReady.bind(router));</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> app;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>为Home.vue、About.vue添加metaInfo属性</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/pages/Home.vue</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">    name: <span class="string">&#x27;home&#x27;</span>,</span><br><span class="line">    metaInfo: &#123;</span><br><span class="line"></span><br><span class="line">        title: <span class="string">&#x27;首页&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// src/pages/About.vue</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">    name: <span class="string">&#x27;about&#x27;</span>,</span><br><span class="line">    metaInfo: &#123;</span><br><span class="line">        title: <span class="string">&#x27;关于&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在模板中使用页面的metaInfo</p>
<p>index-template.html</p>
<figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">&quot;UTF-8&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">name</span>=<span class="string">&quot;viewport&quot;</span> <span class="attr">content</span>=<span class="string">&quot;width=device-width, initial-scale=1.0&quot;</span>&gt;</span></span><br><span class="line">    &#123;&#123;&#123; meta.inject().title.text() &#125;&#125;&#125;</span><br><span class="line">    &#123;&#123;&#123; meta.inject().meta.text() &#125;&#125;&#125;</span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>开启开发服务,访问<code>localhost:3000</code></p>
<img src="/2020/09/27/%E6%90%AD%E5%BB%BAVue-SSR/meta.png" class="">

<p>更多<code>vue-meta</code>使用参照<a target="_blank" rel="noopener" href="https://vue-meta.nuxtjs.org/api/">vue-meta使用</a></p>
<h3 id="数据预取和状态管理"><a href="#数据预取和状态管理" class="headerlink" title="数据预取和状态管理"></a>数据预取和状态管理</h3><blockquote>
<p>在服务器端渲染(SSR)期间，我们本质上是在渲染我们应用程序的”快照”，所以如果应用程序依赖于一些异步数据，那么在开始渲染过程之前，需要先预取和解析好这些数据。</p>
</blockquote>
<blockquote>
<p>另一个需要关注的问题是在客户端，在挂载 (mount) 到客户端应用程序之前，需要获取到与服务器端应用程序完全相同的数据 - 否则，客户端应用程序会因为使用与服务器端应用程序不同的状态，然后导致混合失败。</p>
</blockquote>
<blockquote>
<p>为了解决这个问题，获取的数据需要位于视图组件之外，即放置在专门的数据预取存储容器(data store)或”状态容器(state container)）”中。首先，在服务器端，我们可以在渲染之前预取数据，并将数据填充到 store 中。此外，我们将在 HTML 中序列化(serialize)和内联预置(inline)状态。这样，在挂载(mount)到客户端应用程序之前，可以直接从 store 获取到内联预置(inline)状态。</p>
</blockquote>
<p>这里实现一个文章列表的服务端预取与状态管理</p>
<p>安装 vuex, axios</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install vuex axios --save</span><br></pre></td></tr></table></figure>

<p>创建store<br>src/store/index.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> Vue <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> Vuex <span class="keyword">from</span> <span class="string">&#x27;vuex&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> axios <span class="keyword">from</span> <span class="string">&#x27;axios&#x27;</span>;</span><br><span class="line"></span><br><span class="line">Vue.use(Vuex);</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> createStore = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> Vuex.Store(&#123;</span><br><span class="line">        state: <span class="function">() =&gt;</span> (&#123;</span><br><span class="line">            posts: []</span><br><span class="line">        &#125;),</span><br><span class="line"></span><br><span class="line">        mutations: &#123;</span><br><span class="line">            setPosts (state, data) &#123;</span><br><span class="line">                state.posts = data;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;,</span><br><span class="line"></span><br><span class="line">        actions: &#123;</span><br><span class="line">            <span class="comment">// 在服务端渲染期间务必让action返回一个Promise，服务端渲染时会等待数据返回</span></span><br><span class="line">            <span class="keyword">async</span> getPosts(context) &#123;</span><br><span class="line">                <span class="keyword">const</span> &#123;commit&#125; = context;</span><br><span class="line">                <span class="keyword">const</span> &#123;data&#125; = <span class="keyword">await</span> axios.get(<span class="string">&#x27;https://cnodejs.org/api/v1/topics&#x27;</span>);</span><br><span class="line">                commit(<span class="string">&#x27;setPosts&#x27;</span>, data.data)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>给vue实例注册store<br>scr/app.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123;createStore&#125; <span class="keyword">from</span> <span class="string">&#x27;./store&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">createApp</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> router = createRouter();</span><br><span class="line">    <span class="keyword">const</span> store = createStore(); <span class="comment">// 创建store</span></span><br><span class="line">    <span class="keyword">const</span> app = <span class="keyword">new</span> Vue(&#123;</span><br><span class="line">        router,</span><br><span class="line">        store, <span class="comment">// 注册store</span></span><br><span class="line">        render: <span class="function"><span class="params">h</span> =&gt;</span> h(App)</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">return</span> &#123;app, router, store&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>创建文章页面 并注册路由</p>
<p>src/pages/Posts.vue</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line">    &lt;div <span class="class"><span class="keyword">class</span></span>=<span class="string">&#x27;posts&#x27;</span>&gt;</span><br><span class="line">        &lt;h1&gt;posts list&lt;/h1&gt;</span><br><span class="line">        &lt;ul&gt;</span><br><span class="line">            &lt;li v-<span class="keyword">for</span>=<span class="string">&quot;post in posts&quot;</span> :key=<span class="string">&quot;post.id&quot;</span>&gt;&#123;&#123; post.title &#125;&#125;&lt;/li&gt;</span><br><span class="line">        &lt;/ul&gt;</span><br><span class="line">    &lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line">&lt;script&gt;</span><br><span class="line"><span class="keyword">import</span> &#123;mapState, mapActions&#125; <span class="keyword">from</span> <span class="string">&#x27;vuex&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">    name: <span class="string">&#x27;Posts&#x27;</span>,</span><br><span class="line">    metaInfo: &#123;</span><br><span class="line">        title: <span class="string">&#x27;文章&#x27;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    data() &#123;</span><br><span class="line">        <span class="keyword">return</span> &#123;&#125;;</span><br><span class="line">    &#125;,</span><br><span class="line">    computed: &#123;</span><br><span class="line">        ...mapState([<span class="string">&#x27;posts&#x27;</span>]),</span><br><span class="line">    &#125;,</span><br><span class="line">    methods: &#123;</span><br><span class="line">        ...mapActions([<span class="string">&#x27;getPosts&#x27;</span>]),</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// vue ssr 为服务端提供的特殊的生命周期函数</span></span><br><span class="line">    serverPrefetch() &#123;</span><br><span class="line">        <span class="comment">// 发起action 返回Promise</span></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>.getPosts();</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">&#125;</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure>

<p>这个时候，当我们访问posts路由的页面时，会看到页面渲染出了文章标题列表但是一闪而过，这是因为我们的store中的数据没有同步，服务端和客户端store同步的思路就是，服务端在模板context中保存数据，并将数据通过脚本的方式注入到客户端页面的<code>window.__INITIAL_STATE__</code>中，客户端会拿到这个数据并填充到客户端的store中</p>
<p>修改entry-server.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">async</span> context =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> &#123;app, router, store&#125; = createApp();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> meta = app.$meta();</span><br><span class="line"></span><br><span class="line">    router.push(context.url);</span><br><span class="line"></span><br><span class="line">    context.meta = meta;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">await</span> <span class="keyword">new</span> <span class="built_in">Promise</span>(router.onReady.bind(router));</span><br><span class="line"></span><br><span class="line">    context.rendered = <span class="function">() =&gt;</span> &#123;</span><br><span class="line">        <span class="comment">// renderer 会把 content.state中的数据内联到页面模板中</span></span><br><span class="line">        <span class="comment">// 最终发送给客户端的页面中会包含一段脚本：window.__INITIAL_STATE__ = context.state;</span></span><br><span class="line">        <span class="comment">// 客户端就要把页面中的window.__INITIAL_STATE__ 取出来填充到客户端store中去</span></span><br><span class="line">        context.state = store.state;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> app;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>此时打开浏览器<code>/posts</code>页面，控制台打印查看插入的script数据标签</p>
<img src="/2020/09/27/%E6%90%AD%E5%BB%BAVue-SSR/initialstate.png" class="">

<p>接下来让客户端接管服务端的store</p>
<p>src/entry-client.js</p>
<figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123;app, router, store&#125; = createApp();</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (<span class="built_in">window</span>.__INITIAL_STATE__) &#123;</span><br><span class="line">    store.replaceState(<span class="built_in">window</span>.__INITIAL_STATE__);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这样客户端页面就会接管服务端的数据了</p>
<img src="/2020/09/27/%E6%90%AD%E5%BB%BAVue-SSR/store.png" class="">

</section>
    <!-- Tags START -->
    
      <div class="tags">
        <span>Tags:</span>
        
  <a href="/tags#Vue ssr" >
    <span class="tag-code">Vue ssr</span>
  </a>

      </div>
    
    <!-- Tags END -->
    <!-- NAV START -->
    
  <div class="nav-container">
    <!-- reverse left and right to put prev and next in a more logic postition -->
    
      <a class="nav-left" href="/2020/09/23/webpack%E6%90%AD%E5%BB%BAreact%E8%84%9A%E6%89%8B%E6%9E%B6/">
        <span class="nav-arrow">← </span>
        
          webpack搭建react脚手架
        
      </a>
    
    
      <a class="nav-right" href="/2020/09/29/Gridsome-%E9%9D%99%E6%80%81%E7%BD%91%E9%A1%B5%E7%94%9F%E6%88%90/">
        
          静态网页生成[gridsome]
        
        <span class="nav-arrow"> →</span>
      </a>
    
  </div>

    <!-- NAV END -->
    <!-- 打赏 START -->
    
      <div class="money-like">
        <div class="reward-btn">
          赏
          <span class="money-code">
            <span class="alipay-code">
              <div class="code-image"></div>
              <b>使用支付宝打赏</b>
            </span>
            <span class="wechat-code">
              <div class="code-image"></div>
              <b>使用微信打赏</b>
            </span>
          </span>
        </div>
        <p class="notice">若你觉得我的文章对你有帮助，欢迎点击上方按钮对我打赏</p>
      </div>
    
    <!-- 打赏 END -->
    <!-- 二维码 START -->
    
      <div class="qrcode">
        <canvas id="share-qrcode"></canvas>
        <p class="notice">扫描二维码，分享此文章</p>
      </div>
    
    <!-- 二维码 END -->
    
      <!-- No Comment -->
    
  </article>
  <!-- Article END -->
  <!-- Catalog START -->
  
    <aside class="catalog-container">
  <div class="toc-main">
    <strong class="toc-title">Catalog</strong>
    
      <ol class="toc-nav"><li class="toc-nav-item toc-nav-level-2"><a class="toc-nav-link" href="#%E6%90%AD%E5%BB%BAVUE-SSR"><span class="toc-nav-text">搭建VUE SSR</span></a><ol class="toc-nav-child"><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#%E7%8E%AF%E5%A2%83%E4%BE%9D%E8%B5%96%E5%AE%89%E8%A3%85"><span class="toc-nav-text">环境依赖安装</span></a></li><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#%E6%9C%8D%E5%8A%A1%E7%AB%AF%E7%94%9F%E6%88%90html%E6%96%87%E6%9C%AC"><span class="toc-nav-text">服务端生成html文本</span></a></li><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#html%E6%96%87%E6%9C%AC%E5%8F%91%E9%80%81%E5%88%B0%E6%B5%8F%E8%A7%88%E5%99%A8%EF%BC%8C%E6%90%AD%E5%BB%BAweb%E6%9C%8D%E5%8A%A1"><span class="toc-nav-text">html文本发送到浏览器，搭建web服务</span></a></li><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#%E4%BD%BF%E7%94%A8%E6%A8%A1%E6%9D%BF%E6%B8%B2%E6%9F%93html"><span class="toc-nav-text">使用模板渲染html</span></a></li><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#%E5%90%8C%E6%9E%84"><span class="toc-nav-text">同构</span></a><ol class="toc-nav-child"><li class="toc-nav-item toc-nav-level-4"><a class="toc-nav-link" href="#%E5%88%9B%E5%BB%BA%E6%BA%90%E4%BB%A3%E7%A0%81%E6%96%87%E4%BB%B6"><span class="toc-nav-text">创建源代码文件</span></a></li><li class="toc-nav-item toc-nav-level-4"><a class="toc-nav-link" href="#%E5%AE%89%E8%A3%85%E4%BE%9D%E8%B5%96"><span class="toc-nav-text">安装依赖</span></a></li><li class="toc-nav-item toc-nav-level-4"><a class="toc-nav-link" href="#webpack%E9%85%8D%E7%BD%AE"><span class="toc-nav-text">webpack配置</span></a></li><li class="toc-nav-item toc-nav-level-4"><a class="toc-nav-link" href="#%E9%85%8D%E7%BD%AE%E6%89%93%E5%8C%85%E5%91%BD%E4%BB%A4"><span class="toc-nav-text">配置打包命令</span></a><ol class="toc-nav-child"><li class="toc-nav-item toc-nav-level-5"><a class="toc-nav-link" href="#%E8%84%9A%E6%9C%AC%E6%B5%8B%E8%AF%95"><span class="toc-nav-text">脚本测试</span></a></li></ol></li><li class="toc-nav-item toc-nav-level-4"><a class="toc-nav-link" href="#%E5%90%AF%E5%8A%A8%E5%90%8C%E6%9E%84%E6%9C%8D%E5%8A%A1"><span class="toc-nav-text">启动同构服务</span></a></li></ol></li><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#%E9%85%8D%E7%BD%AE%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83"><span class="toc-nav-text">配置生产环境</span></a></li><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#%E9%85%8D%E7%BD%AE%E8%B7%AF%E7%94%B1"><span class="toc-nav-text">配置路由</span></a></li><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#%E9%85%8D%E7%BD%AE%E4%B8%8D%E5%90%8C%E7%95%8C%E9%9D%A2%E7%9A%84Head%E6%A0%87%E7%AD%BE"><span class="toc-nav-text">配置不同界面的Head标签</span></a></li><li class="toc-nav-item toc-nav-level-3"><a class="toc-nav-link" href="#%E6%95%B0%E6%8D%AE%E9%A2%84%E5%8F%96%E5%92%8C%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86"><span class="toc-nav-text">数据预取和状态管理</span></a></li></ol></li></ol>
    
  </div>
</aside>
  
  <!-- Catalog END -->
</main>

<script>
  (function () {
    var url = 'https://github.com/alongithub/2020/09/27/搭建Vue-SSR/';
    var banner = ''
    if (banner !== '' && banner !== 'undefined' && banner !== 'null') {
      $('#article-banner').css({
        'background-image': 'url(' + banner + ')'
      })
    } else {
      $('#article-banner').geopattern(url)
    }
    $('.header').removeClass('fixed-header')

    // error image
    $(".markdown-content img").on('error', function() {
      $(this).attr('src', '/css/images/error_icon.png')
      $(this).css({
        'cursor': 'default'
      })
    })

    // zoom image
    $(".markdown-content img").on('click', function() {
      var src = $(this).attr('src')
      if (src !== '/css/images/error_icon.png') {
        var imageW = $(this).width()
        var imageH = $(this).height()

        var zoom = ($(window).width() * 0.95 / imageW).toFixed(2)
        zoom = zoom < 1 ? 1 : zoom
        zoom = zoom > 2 ? 2 : zoom
        var transY = (($(window).height() - imageH) / 2).toFixed(2)

        $('body').append('<div class="image-view-wrap"><div class="image-view-inner"><img src="'+ src +'" /></div></div>')
        $('.image-view-wrap').addClass('wrap-active')
        $('.image-view-wrap img').css({
          'width': `${imageW}`,
          'transform': `translate3d(0, ${transY}px, 0) scale3d(${zoom}, ${zoom}, 1)`
        })
        $('html').css('overflow', 'hidden')

        $('.image-view-wrap').on('click', function() {
          $(this).remove()
          $('html').attr('style', '')
        })
      }
    })
  })();
</script>


  <script>
    var qr = new QRious({
      element: document.getElementById('share-qrcode'),
      value: document.location.href
    });
  </script>






    <div class="scroll-top">
  <span class="arrow-icon"></span>
</div>
    <footer class="app-footer">
  <p class="copyright">
    Copyright&copy; 2019-2022 along
    <br>
    <a href="https://beian.miit.gov.cn/" target="_blank">京ICP备2020044791号-1</a>
    <!-- <br>
    Theme by <a href="https://github.com/alongithub">alongithub</a>
     -->
  </p>
</footer>

<script>
  function async(u, c) {
    var d = document, t = 'script',
      o = d.createElement(t),
      s = d.getElementsByTagName(t)[0];
    o.src = u;
    if (c) { o.addEventListener('load', function (e) { c(null, e); }, false); }
    s.parentNode.insertBefore(o, s);
  }
</script>
<script>
  async("/cdn.along/fastclick/1.0.6/fastclick.min.js", function(){
    FastClick.attach(document.body);
  })
</script>

<script>
  var hasLine = 'true';
  async("/cdn.along/highlight.js/9.12.0/highlight.min.js", function(){
    $('figure pre').each(function(i, block) {
      var figure = $(this).parents('figure');
      if (hasLine === 'false') {
        figure.find('.gutter').hide();
      }
      hljs.configure({useBR: true});
      var lang = figure.attr('class').split(' ')[1] || 'code';
      var codeHtml = $(this).html();
      var codeTag = document.createElement('code');
      codeTag.className = lang;
      codeTag.innerHTML = codeHtml;
      $(this).attr('class', '').empty().html(codeTag);
      figure.attr('data-lang', lang.toUpperCase());
      hljs.highlightBlock(block);
    });
  })
</script>
<!-- Baidu Tongji -->


<script src="/js/script.js"></script>


  </body>
</html>